/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  decodeData,
  getMimeParts,
  parseMime,
  MimeMail,
  getEnvelopeFromMailHeader,
} from "../utils/mime";
import {
  IFolder,
  IMail,
  IStore,
  EmailAddress,
  MailStructure,
  IBuffer,
  Mime,
  MailBasic,
  MailPriority,
  MailHeader,
  StoreFeature,
} from "../api";
import { CMError, ErrorCode, MailAttribute, } from "../api";
import { EventHandler } from "../utils/common";
import { getLogger } from "../utils/log";
import { createBuffer } from "../utils/file_stream";
import { decodeCharsetStream } from '../utils/encodings';

const logger = getLogger("mail");

export class Mail implements IMail {
  folder: IFolder;
  id: string;
  store: IStore;
  _events: EventHandler = new EventHandler();

  _mailBasic?: MailBasic;
  _mail?: MimeMail;
  _headers?: MailHeader;

  _html?: string;
  _plain?: string;
  _initPromise?: Promise<void>;
  _ready: boolean = false;

  constructor(id: string, folder: IFolder, store: IStore) {
    this.id = id;
    this.folder = folder;
    this.store = store;
  }

  toLogString(): string {
    return `mail[${this.folder.getFullName()}-${this.id}]`
  }

  getId(): string {
    return this.id;
  }

  on(
    event: "attr-changed" | "seen-changed" | "flagged-changed",
    handler: Function
  ): void {
    this._events.on(event, handler);
  }

  async parseFullMail() {
    const attributes = await this.store.getMailAttributes(
      this.folder.getFullName(),
      this.id
    )
    const eml = await this.store.getMailRaw(
      this.folder.getFullName(),
      this.id
    );
    const mime = await parseMime(eml);
    const mail = new MimeMail(mime);
    this._mail = mail;
    this._headers = mime.headers;
    this._mailBasic = {
      id: this.id,
      envelope: mail.getEnvelope(),
      structure: mail.getStructure(),
      attributes,
      size: await eml.getSizeRaw()
    };
    const part = mail.getHtmlPart();
    if (part) {
      const b = await decodeData((part as Mime).body, part.encoding);
      this._html = await decodeCharsetStream(b, part.contentType.params.charset);
    } else {
      this._html = '';
    }

    const part2 = mail.getPlainPart();
    if (part2) {
      const b = await decodeData((part2 as Mime).body, part2.encoding);
      this._plain = await decodeCharsetStream(b, part2.contentType.params.charset);
    } else {
      this._plain = '';
    }
  }

  async parseMailBasic() {
    const mailBasic = await this.store.getMailBasic(
      this.folder.getFullName(),
      this.id
    );
    logger.trace(`[get mail basic ${this.toLogString()}]`)
    this._mailBasic = mailBasic;
  }

  async init(): Promise<void> {
    if (this._initPromise) {
      logger.debug("init -- waiting promise", this);
      return this._initPromise;
    }

    if (this.store.hasFeature(StoreFeature.MailPart)) {
      this._initPromise = this.parseMailBasic()
        .catch((e: unknown) => {
          logger.error("get mail basic failed", this, e);
          return this.parseFullMail();
        })
    } else {
      this._initPromise = this.parseFullMail()
    }

    this._initPromise = this._initPromise.then(() => {
      this._ready = true;
      this._initPromise = undefined;
    })
    .catch((e: unknown) => {
      this._initPromise = undefined;
      logger.error("get mail full failed", this, e);
      throw new CMError(`parse mail failed ${this.toLogString()}`, ErrorCode.PARSE_MAIL_FAILED, e)
    })
    return this._initPromise;
  }

  async getFrom(): Promise<EmailAddress> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.envelope.from;
  }

  async getTo(): Promise<EmailAddress[]> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.envelope.to;
  }

  async getCc(): Promise<EmailAddress[]> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.envelope.cc;
  }

  async getBcc(): Promise<EmailAddress[]> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.envelope.bcc;
  }

  async getSubject(): Promise<string> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.envelope.subject;
  }

  async getDate(): Promise<Date> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.envelope.date;
  }

  async getSize(): Promise<number> {
    if (!this._ready) {
      await this.init();
    }
    return this._mailBasic?.size;
  }

  async getPriority(): Promise<MailPriority> {
    let priority = MailPriority.NORMAL;
    let v = await this.getHeader("X-Priority");
    if (v) {
      switch (v) {
        case '1':
        case '2':
          priority = MailPriority.HIGH;
          break;
        case '4':
        case '5':
          priority = MailPriority.LOW;
          break;
        case '3':
        default:
          priority = MailPriority.NORMAL;
          break;
      }
    } else {
      v = await this.getHeader("X-MSMail-Priority");
      if (v) {
        switch (v.toLowerCase()) {
          case 'low':
            priority = MailPriority.LOW;
            break;
          case 'high':
            priority = MailPriority.HIGH;
            break;
          case 'low':
          default:
            priority = MailPriority.NORMAL;
            break;
        }
      }
    }
    return priority;
  }

  getMessageId(): Promise<string | undefined> {
    if (this._mailBasic) {
      return Promise.resolve(this._mailBasic.envelope.messageId);
    }
    return this.getHeader('Message-ID');
  }

  async getHeader(headerName: string): Promise<string | undefined> {
    if (!this._headers) {
      this._headers = await this.store.getMailHeader(
        this.folder.getFullName(),
        this.id,
      )
    }
    return this._headers.get(headerName.toLowerCase());
  }

  async getAllHeaders(): Promise<MailHeader> {
    if (!this._headers) {
      this._headers = await this.store.getMailHeader(
        this.folder.getFullName(),
        this.id,
      )
    }
    return this._headers;
  }

  async hasAttachment(): Promise<boolean> {
    const attachments = await this.getAttachmentInfoList();
    return attachments.length > 0;
  }

  async getAttachmentInfoList(): Promise<MailStructure[]> {
    if (!this._ready) {
      await this.init();
    }
    const struct = this._mailBasic?.structure;
    if (!struct) {
      return [];
    }
    return getMimeParts(struct,
      (mime) => !!mime.disposition?.type.startsWith("attachment"))
  }

  async getInlineImageInfoList(): Promise<MailStructure[]> {
    if (!this._ready) {
      await this.init();
    }
    const struct = this._mailBasic?.structure;
    if (!struct) {
      return [];
    }
    return getMimeParts(struct, (mime) => {
      let isInline = !!(mime.disposition?.type == "inline");
      if (!isInline && mime.contentType.type == 'image' && mime.contentId) {
        isInline = true;
      }
      return isInline;
    })
  }

  async getAttachment(index: number): Promise<IBuffer> {
    const structs = await this.getAttachmentInfoList();
    if (structs.length == 0 || structs.length <= index) {
      throw new CMError("attachment not found", ErrorCode.ATTACHMENT_NOT_FOUND);
    }
    const struct = structs[index];
    return this._getAttachment(struct);
  }

  async getInlineImage(index: number): Promise<IBuffer> {
    const structs = await this.getInlineImageInfoList();
    if (structs.length == 0 || structs.length <= index) {
      throw new CMError("inline image not found", ErrorCode.INLINE_IMAGE_NOT_FOUND);
    }
    const struct = structs[index];
    return this._getAttachment(struct);
  }

  async _getAttachment(struct: MailStructure): Promise<IBuffer> {
    const buff = await this.store.getMailPartContent(
      this.folder.getFullName(),
      this.id,
      struct.partId
    );
    const res = decodeData(buff, struct.encoding);
    return res;
  }

  async getText(textType: "html" | "plain"): Promise<string> {
    if (!this._ready) {
      await this.init();
    }
    const struct = this._mailBasic?.structure;
    // logger.debug(JSON.stringify(struct, null, 2));
    if (!struct) {
      // todo: 重新获取整份邮件进行解析
      return "";
    }
    if (textType == "html" && this._html) {
      return this._html;
    } else if (textType == "plain" && this._plain) {
      return this._plain;
    }
    const parts = getMimeParts(struct, (struct: MailStructure) => {
      const ms = struct;
      if (ms.disposition?.type == "attachment") {
        return false;
      }
      return ms.contentType.type == "text" && ms.contentType.subType == textType;
    });
    if (parts.length === 0) {
      const s = `text/${textType} not found`;
      logger.error(s);
      return Promise.reject(s);
    }
    const part = parts[0];
    const partId = part.partId;
    const stream = await this.store.getMailPartContent(
      this.folder.getFullName(),
      this.id,
      partId
    );
    logger.debug("start decoding mime body")
    const buff = await decodeData(stream, part.encoding);
    const s = await decodeCharsetStream(buff, part.contentType.params.charset);
    logger.debug("end decoding mime body")

    return s;
  }

  getHtml(): Promise<string> {
    return this.getText("html").catch(e => {
      logger.warn("get html failed", e);
      return "";
    });
  }

  getPlain(): Promise<string> {
    return this.getText("plain").catch(e => {
      logger.warn("get plain failed", e);
      return "";
    });
  }

  async getDigest(n: number): Promise<string> {
    const plain = await this.getPlain();
    return plain.substring(0, n);
  }

  async hasAttribute(attr: MailAttribute): Promise<boolean> {
    if (!this._ready) {
      await this.init();
    }
    const attributes = this._mailBasic?.attributes;
    if (!attributes) {
      return false;
    }
    return attributes.includes(attr);
  }

  async setAttributes(
    attrs: MailAttribute[],
    modifyType: "+" | "-" | "" = ""
  ): Promise<void> {
    logger.trace(`[__imap] [syncstore set attr]`)
    const attributes = await this.store.setMailAttributes(
      this.folder.getFullName(), this.id, attrs, modifyType)
    logger.trace(`[__imap] [cache set attr]`)
    if (this._mailBasic) {
      this._mailBasic.attributes = attributes;
    }
  }

  async isSeen(): Promise<boolean> {
    return this.hasAttribute(MailAttribute.Seen);
  }

  setSeen(isSeen: boolean): Promise<void> {
    return this.setAttributes([MailAttribute.Seen], isSeen ? "+" : "-");
  }

  isFlagged(): Promise<boolean> {
    return this.hasAttribute(MailAttribute.Flagged);
  }

  setFlagged(isFlagged: boolean): Promise<void> {
    return this.setAttributes([MailAttribute.Flagged], isFlagged ? "+" : "-");
  }

  isAnswered(): Promise<boolean> {
    return this.hasAttribute(MailAttribute.Answered);
  }

  setAnswered(isAnswered: boolean): Promise<void> {
    return this.setAttributes([MailAttribute.Answered], isAnswered ? "+" : "-");
  }

  async getAttributes(): Promise<MailAttribute[]> {
    if (!this._ready) {
      await this.init();
    }
    const attributes = this._mailBasic?.attributes;
    if (!attributes) {
      return [];
    }
    return [...attributes];
  }

  getFolder(): IFolder {
    return this.folder;
  }

  async delete(): Promise<void> {
    await this.store.deleteMail(this.folder.getFullName(), this.id);
    await this.folder.refresh();
    return
  }

  copyTo(folder: IFolder): Promise<string> {
    return this.store.copyMail(
      this.folder.getFullName(),
      this.id,
      folder.getFullName()
    );
  }

  async moveTo(folder: IFolder): Promise<string> {
    let mid = await this.store.moveMail(
      this.folder.getFullName(),
      this.id,
      folder.getFullName()
    )
    let isNew = this.isSeen();
    if(isNew) {
      await this.folder.refresh();
      await folder.refresh();
    }
    return mid;
  }
}
